namespace FluentDataContract.Adapters.Json
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Attributes;
    using Internal;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using ReferenceLoopHandling = ReferenceLoopHandling;

    public class JsonContractFactory
    {
        private AttributedDataContractFactory _objectContractFactory;
        private AttributedDataContractFactory ObjectContractFactory { get { return _objectContractFactory ?? (_objectContractFactory = new AttributedDataContractFactory()); } }

        private readonly InstanceCache _cache = new InstanceCache();

        public JsonObjectContract ModifyExistingContract(Type type, JsonObjectContract existingContract)
        {
            var objectContract = ObjectContractFactory.GetObjectContract(type);
            if (objectContract == null)
                return existingContract;
            
            object jsonContract;
            if (!_cache.TryGet(type, out jsonContract))
            {
                //jsonContract = ConvertToJsonContract(objectContract);
                jsonContract = ModifyContract(existingContract, objectContract);
                _cache.Add(type, jsonContract);
            }

            return jsonContract as JsonObjectContract;
        }

        public virtual JsonObjectContract GetJsonContract(Type type)
        {
            var objectContract = ObjectContractFactory.GetObjectContract(type);
            if (objectContract == null)
                return null;

            object jsonContract;
            if(!_cache.TryGet(type, out jsonContract))
            {
                jsonContract = ConvertToJsonContract(objectContract);
                _cache.Add(type, jsonContract);
            }

            return (JsonObjectContract)jsonContract;
        }

        private static JsonObjectContract ModifyContract(JsonObjectContract existingContract, IObjectContract objectContract)
        {
            var result = existingContract;
            result.ItemReferenceLoopHandling = ConvertReferenceLoopHanding(objectContract.ReferenceLoopHandling);
            result.MemberSerialization = MemberSerialization.OptIn;

            var properties = new List<JsonProperty>();

            for(var i = 0; i < result.Properties.Count; i++)
            {
                var jProperty = result.Properties[i];
                if (!objectContract.Properties.Any(o => jProperty.PropertyName == o.PropertyName))
                    continue;

                var oProperty = objectContract.Properties.First(o => jProperty.PropertyName == o.PropertyName);
                jProperty.DefaultValueHandling = ConvertDefaultValueHandling(oProperty.IncludeDefaultValues);
                jProperty.NullValueHandling = ConvertNullValueHandling(oProperty.IncludeNullValues);
                jProperty.ReferenceLoopHandling = ConvertReferenceLoopHanding(oProperty.ReferenceLoopHandling);
                jProperty.IsReference = oProperty.IsReference;
                jProperty.PropertyName = oProperty.PropertyName;
                jProperty.PropertyType = oProperty.Type;
                jProperty.Order = oProperty.Order;

                properties.Add(jProperty);
            }

            result.Properties.Clear();

            foreach(var jsonProperty in properties)
                result.Properties.AddProperty(jsonProperty);

            return result;
        }

        private static JsonContract ConvertToJsonContract(IObjectContract objectContract)
        {
            var result = new JsonObjectContract(objectContract.Type)
            {
                    ItemReferenceLoopHandling = ConvertReferenceLoopHanding(objectContract.ReferenceLoopHandling), 
                    MemberSerialization = MemberSerialization.OptIn
            };

            foreach(var p in objectContract.Properties)
            {
                var jsonProperty = new JsonProperty
                {
                        DefaultValueHandling = ConvertDefaultValueHandling(p.IncludeDefaultValues), 
                        NullValueHandling = ConvertNullValueHandling(p.IncludeNullValues), 
                        ReferenceLoopHandling = ConvertReferenceLoopHanding(p.ReferenceLoopHandling),
                        IsReference = p.IsReference, 
                        PropertyName = p.PropertyName, 
                        PropertyType = p.Type, 
                        Order = p.Order
                };

                result.Properties.Add(jsonProperty);
            }

            return result;
        }

        private static Newtonsoft.Json.ReferenceLoopHandling ConvertReferenceLoopHanding(ReferenceLoopHandling referenceLoopHandling)
        {
            switch(referenceLoopHandling)
            {
                case ReferenceLoopHandling.Error:
                    return Newtonsoft.Json.ReferenceLoopHandling.Error;

                case ReferenceLoopHandling.Exclude:
                    return Newtonsoft.Json.ReferenceLoopHandling.Ignore;

                case ReferenceLoopHandling.Include:
                    return Newtonsoft.Json.ReferenceLoopHandling.Serialize;

                default:
                    return Newtonsoft.Json.ReferenceLoopHandling.Ignore;
            }
        }

        private static NullValueHandling ConvertNullValueHandling(bool includeNullValues)
        {
            return includeNullValues ? NullValueHandling.Include : NullValueHandling.Ignore;
        }

        private static DefaultValueHandling ConvertDefaultValueHandling(bool includeDefaultValues)
        {
            return includeDefaultValues ? DefaultValueHandling.Include : DefaultValueHandling.Ignore;
        }
    }
}